﻿using JESAI.DistributedLock.Internal;
using JESAI.DistributedLock.Internal.Data;
using MySqlConnector;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace JESAI.DistributedLock.MySql;

/// <summary>
/// 实现 <see cref="IDbSynchronizationStrategy{TLockCookie}"/> 使用用户级锁定功能。请参阅
/// https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html
/// </summary>
internal class MySqlUserLock : IDbSynchronizationStrategy<object>
{
    // 匹配 SqlApplicationLock
    private const int AlreadyHeldReturnCode = 103;
    // 请参阅记录在https://mariadb.com/kb/en/get_lock/当GET_LOCK返回NULL时
    private const int GetLockErrorReturnCode = 104;

    private static readonly object Cookie = new();

    public bool IsUpgradeable => false;

    public async ValueTask ReleaseAsync(DatabaseConnection connection, string resourceName, object lockCookie)
    {
        using var command = connection.CreateCommand();
        command.SetCommandText("DO RELEASE_LOCK(@name)");
        command.AddParameter("name", resourceName);
        await command.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
    }

    public async ValueTask<object?> TryAcquireAsync(DatabaseConnection connection, string resourceName, TimeoutValue timeout, CancellationToken cancellationToken)
    {
        try
        {
            using var command = connection.CreateCommand();
            command.SetCommandText($"SELECT CASE WHEN IS_USED_LOCK(@name) = CONNECTION_ID() THEN {AlreadyHeldReturnCode} ELSE IFNULL(GET_LOCK(@name, @timeoutSeconds), {GetLockErrorReturnCode}) END");
            command.AddParameter("name", resourceName);
            // 注意：-1适用于MySQL，但不适用于MariaDB(https://stackoverflow.com/questions/49792089/set-infinite-timeout-get-lock-in-mariadb/49809919)
            command.AddParameter("timeoutSeconds", timeout.IsInfinite ? 0xFFFFFFFF : timeout.InSeconds);

            // 需要转换，因为该值在MariaDB上返回为int，在MySQL上返回为long
            var acquireCommandResult = Convert.ToInt64(await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false));
            switch (acquireCommandResult)
            {
                case 0: // timeout
                    return null;
                case 1: // success
                    return Cookie;
                case AlreadyHeldReturnCode:
                    if (timeout.IsZero) { return null; }
                    if (timeout.IsInfinite) { throw new DeadlockException("Attempted to acquire a lock that is already held on the same connection"); }
                    await SyncViaAsync.Delay(timeout, cancellationToken).ConfigureAwait(false);
                    return null;
                case GetLockErrorReturnCode:
                    cancellationToken.ThrowIfCancellationRequested(); // this error can also indicate cancellation in MariaDB
                    throw new InvalidOperationException("An error occurred such as running out of memory on the thread or a mysqladmin kill when trying to acquire the lock");
                default:
                    throw new InvalidOperationException($"Unexpected return code {acquireCommandResult}");
            }
        }
        catch (MySqlException ex)
            // 来自 https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_user_lock_deadlock
            when ((ex.Number == 3058 && ex.SqlState == "HY000")
                // 来自 https://mariadb.com/kb/en/mariadb-error-codes/
                || (ex.Number == 1213 && ex.SqlState == "40001"))
        {
            throw new DeadlockException($"The request for the distributed lock failed with deadlock exit code {ex.Number}", ex);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            // 如果命令被取消，我相信收购在取消之前刚刚完成的可能性很小。
            // 在这种情况下，我很确定它不会被回滚。因此，为了安全起见，我们发布了一份声明
            await this.ReleaseAsync(connection, resourceName, Cookie).ConfigureAwait(false);
            throw;
        }
    }
}
